package main

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/hyperledger/fabric-contract-api-go/contractapi"
)

type Marble struct {
	ObjectType string `json:"docType"`
	Name       string `json:"name"`
	Color      string `json:"color"`
	Size       int    `json:"size"`
	Owner      string `json:"owner"`
}

type MarblePrivateDetails struct {
	ObjectType string `json:"docType"`
	Name       string `json:"name"`
	Price      int    `json:"price"`
}

type SmartContract struct {
	contractapi.Contract
}

func (s *SmartContract) InitMarble(ctx contractapi.TransactionContextInterface) error {

	transMap, err := ctx.GetStub().GetTransient()
	if err != nil {
		return fmt.Errorf("Error getting transient: " + err.Error())
	}

	transientMarbleJSON, ok := transMap["marble"]
	if !ok {
		return fmt.Errorf("marble not found in the transient map")
	}

	type marbleTransientInput struct {
		Name  string `json:"name"`
		Color string `json:"color"`
		Size  int    `json:"size"`
		Owner string `json:"owner"`
		Price int    `json:"price"`
	}

	var marbleInput marbleTransientInput
	err = json.Unmarshal(transientMarbleJSON, &marbleInput)
	if err != nil {
		return fmt.Errorf("failed to unmarshal JSON: %s", err.Error())
	}
	if len(marbleInput.Name) == 0 {
		return fmt.Errorf("name field must be a non-empty string")
	}
	if len(marbleInput.Color) == 0 {
		return fmt.Errorf("color field must be a non-empty string")
	}
	if marbleInput.Size <= 0 {
		return fmt.Errorf("size field must be a positice integer")
	}
	if len(marbleInput.Owner) == 0 {
		return fmt.Errorf("owner field must be a non-empty string")
	}
	if marbleInput.Price <= 0 {
		return fmt.Errorf("price field must be a positive integer")
	}

	marbleAsBytes, err := ctx.GetStub().GetPrivateData("collectionMarbles", marbleInput.Name)
	if err != nil {
		return fmt.Errorf("Failed to get marble: " + err.Error())
	} else if marbleAsBytes != nil {
		fmt.Println("This marble already exists: " + marbleInput.Name)
		return fmt.Errorf("This marble already exists: " + marbleInput.Name)
	}

	marble := &Marble{
		ObjectType: "Marble",
		Name:       marbleInput.Name,
		Color:      marbleInput.Color,
		Size:       marbleInput.Size,
		Owner:      marbleInput.Owner,
	}
	marbleJSONasBytes, err := json.Marshal(marble)
	if err != nil {
		return fmt.Errorf(err.Error())
	}

	err = ctx.GetStub().PutPrivateData("collectionMarbles", marbleInput.Name, marbleJSONasBytes)
	if err != nil {
		return fmt.Errorf("failed to put Marble: %s", err.Error())
	}

	marblePrivateDetails := &MarblePrivateDetails{
		ObjectType: "MarblePrivateDetails",
		Name:       marbleInput.Name,
		Price:      marbleInput.Price,
	}
	marblePrivateDetailsAsBytes, err := json.Marshal(marblePrivateDetails)
	if err != nil {
		return fmt.Errorf(err.Error())
	}
	err = ctx.GetStub().PutPrivateData("collectionMarblePrivateDetails", marbleInput.Name, marblePrivateDetailsAsBytes)
	if err != nil {
		return fmt.Errorf("failed to put Marble private details: %s", err.Error())
	}

	indexName := "color~name"
	colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(indexName, []string{marble.Color, marble.Name})
	if err != nil {
		return err
	}

	value := []byte{0x00}
	err = ctx.GetStub().PutPrivateData("collectionMarbles", colorNameIndexKey, value)
	if err != nil {
		return err
	}

	return nil
}

func (s *SmartContract) ReadMarble(ctx contractapi.TransactionContextInterface, marbleID string) (*Marble, error) {
	marbleJSON, err := ctx.GetStub().GetPrivateData("collectionMarbles", marbleID)
	if err != nil {
		return nil, fmt.Errorf("failed to read from marble %s", err.Error())
	}
	if marbleJSON == nil {
		return nil, fmt.Errorf("%s does not exist", marbleID)
	}
	marble := new(Marble)
	_ = json.Unmarshal(marbleJSON, marble)

	return marble, nil
}

func (s *SmartContract) ReadMarblePrivateDetails(ctx contractapi.TransactionContextInterface, marbleID string) (*MarblePrivateDetails, error) {
	marbleDetailsJSON, err := ctx.GetStub().GetPrivateData("collectionMarblePrivateDetails", marbleID)
	if err != nil {
		return nil, fmt.Errorf("failed to read from marble details %s", err.Error())
	}
	if marbleDetailsJSON == nil {
		return nil, fmt.Errorf("%s does not exist", marbleID)
	}

	marbleDetails := new(MarblePrivateDetails)
	_ = json.Unmarshal(marbleDetailsJSON, marbleDetails)

	return marbleDetails, nil
}

func (s *SmartContract) Delete(ctx contractapi.TransactionContextInterface) error {
	transMap, err := ctx.GetStub().GetTransient()
	if err != nil {
		return fmt.Errorf("Error getting transient: " + err.Error())
	}

	transientDeleteMarbleJSON, ok := transMap["marble_delete"]
	if !ok {
		return fmt.Errorf("marble to delete not found in the transient map")
	}

	type marbleDelete struct {
		Name string `json:"name"`
	}

	var marbleDeleteInput marbleDelete
	err = json.Unmarshal(transientDeleteMarbleJSON, &marbleDeleteInput)
	if err != nil {
		return fmt.Errorf("failed to unmarshal JSON: %s", err.Error())
	}
	if len(marbleDeleteInput.Name) == 0 {
		return fmt.Errorf("name field must be a non-empty string")
	}

	valAsbytes, err := ctx.GetStub().GetPrivateData("collectionMarbles", marbleDeleteInput.Name)
	if err != nil {
		return fmt.Errorf("failed to read marble: %s", err.Error())
	}
	if valAsbytes == nil {
		return fmt.Errorf("marble private details does not exist: %s", marbleDeleteInput.Name)
	}

	var marbleToDelete Marble
	err = json.Unmarshal([]byte(valAsbytes), &marbleToDelete)
	if err != nil {
		return fmt.Errorf("failed to unmarshal JSON: %s", err.Error())
	}

	err = ctx.GetStub().DelPrivateData("collectionMarbles", marbleDeleteInput.Name)
	if err != nil {
		return fmt.Errorf("Failed to delete state:" + err.Error())
	}

	indexName := "color~name"
	colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(indexName, []string{marbleToDelete.Color, marbleToDelete.Name})
	if err != nil {
		return err
	}
	err = ctx.GetStub().DelPrivateData("collectionMarbles", colorNameIndexKey)
	if err != nil {
		return fmt.Errorf("Failed to delete marble:" + err.Error())
	}

	err = ctx.GetStub().DelPrivateData("collectionMarblePrivateDetails", marbleDeleteInput.Name)
	if err != nil {
		return err
	}
	return nil
}

func (s *SmartContract) TransferMarble(ctx contractapi.TransactionContextInterface) error {
	transMap, err := ctx.GetStub().GetTransient()
	if err != nil {
		return fmt.Errorf("Error getting transient: " + err.Error())
	}

	transientTransferMarbleJSON, ok := transMap["marble_owner"]
	if !ok {
		return fmt.Errorf("marble owner not found in the transient map")
	}

	type marbleTransferTransientInput struct {
		Name  string `json:"name"`
		Owner string `json:"owner"`
	}

	var marbleTransferInput marbleTransferTransientInput
	err = json.Unmarshal(transientTransferMarbleJSON, &marbleTransferInput)
	if err != nil {
		return fmt.Errorf("failed to unmarshal JSON: %s", err.Error())
	}

	if len(marbleTransferInput.Name) == 0 {
		return fmt.Errorf("name field must be a non-empty string")
	}
	if len(marbleTransferInput.Owner) == 0 {
		return fmt.Errorf("owner field must be a non-empty string")
	}

	marbleAsBytes, err := ctx.GetStub().GetPrivateData("collectionMarbles", marbleTransferInput.Name)
	if err != nil {
		return fmt.Errorf("Failed to get marble:" + err.Error())
	} else if marbleAsBytes == nil {
		return fmt.Errorf("Marble does not exist: " + marbleTransferInput.Name)
	}

	marbletoTransfer := Marble{}
	err = json.Unmarshal(marbleAsBytes, &marbletoTransfer)
	if err != nil {
		return fmt.Errorf("failed to unmarshal JSON: %s", err.Error())
	}

	marbletoTransfer.Owner = marbleTransferInput.Owner

	marbleJSONasBytes, _ := json.Marshal(marbletoTransfer)
	err = ctx.GetStub().PutPrivateData("collectionMarbles", marbletoTransfer.Name, marbleJSONasBytes)
	if err != nil {
		return err
	}

	return nil
}

func (s *SmartContract) GetMarblesByRange(ctx contractapi.TransactionContextInterface, startKey string, endKey string) ([]Marble, error) {
	resultsIterator, err := ctx.GetStub().GetPrivateDataByRange("collectionMarbles", startKey, endKey)
	if err != nil {
		return nil, err
	}
	defer resultsIterator.Close()

	results := []Marble{}
	for resultsIterator.HasNext() {
		response, err := resultsIterator.Next()
		if err != nil {
			return nil, err
		}

		newMarble := new(Marble)

		err = json.Unmarshal(response.Value, newMarble)
		if err != nil {
			return nil, err
		}
		results = append(results, *newMarble)
	}
	return results, nil
}

func (s *SmartContract) QueryMarblesByOwner(ctx contractapi.TransactionContextInterface, owner string) ([]Marble, error) {
	ownerString := strings.ToLower(owner)

	queryString := fmt.Sprintf("{\"selector\":{\"docType\":\"marble\",\"owner\":\"%s\"}}", ownerString)

	queryResults, err := s.getQueryResultForQueryString(ctx, queryString)
	if err != nil {
		return nil, err
	}
	return queryResults, nil
}

func (s *SmartContract) QueryMarbles(ctx contractapi.TransactionContextInterface, queryString string) ([]Marble, error) {
	queryResults, err := s.getQueryResultForQueryString(ctx, queryString)
	if err != nil {
		return nil, err
	}
	return queryResults, nil
}
func (s *SmartContract) getQueryResultForQueryString(ctx contractapi.TransactionContextInterface, queryString string) ([]Marble, error) {
	resultsIterator, err := ctx.GetStub().GetPrivateDataQueryResult("collectionMarbles", queryString)
	if err != nil {
		return nil, err
	}
	defer resultsIterator.Close()

	results := []Marble{}

	for resultsIterator.HasNext() {
		response, err := resultsIterator.Next()
		if err != nil {
			return nil, err
		}

		newMarble := new(Marble)

		err = json.Unmarshal(response.Value, newMarble)
		if err != nil {
			return nil, err
		}

		results = append(results, *newMarble)
	}
	return results, nil
}

func (s *SmartContract) GetMarbleHash(ctx contractapi.TransactionContextInterface, collection string, marbleID string) (string, error) {
	hashAsbytes, err := ctx.GetStub().GetPrivateDataHash(collection, marbleID)
	if err != nil {
		return "", fmt.Errorf("Failed to get public data hash for marble:" + err.Error())
	} else if hashAsbytes == nil {
		return "", fmt.Errorf("Marble does not exist: " + marbleID)
	}
	return string(hashAsbytes), nil
}

func main() {
	chaincode, err := contractapi.NewChaincode(new(SmartContract))

	if err != nil {
		fmt.Printf("Error creating private marbles chaincode: %s", err.Error())
		return
	}

	if err := chaincode.Start(); err != nil {
		fmt.Printf("Error starting private marbles chaincode: %s", err.Error())
	}
}
